/*
 * Copyright 2004-2005 The Apache Software Foundation or its licensors,
 *                     as applicable.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.core.query.lucene;

import org.apache.commons.collections.iterators.IteratorChain;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.name.QName;
import org.apache.jackrabbit.name.NamespaceResolver;
import org.apache.jackrabbit.name.NoPrefixDeclaredException;

import javax.jcr.query.QueryResult;
import javax.jcr.query.RowIterator;
import javax.jcr.RepositoryException;
import javax.jcr.NodeIterator;
import javax.jcr.Session;
import javax.jcr.Node;
import java.util.Iterator;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;

/**
 * Implements a query result that traverses the whole workspace and returns
 * the nodes in document order.
 */
public class WorkspaceTraversalResult implements QueryResult {

    /**
     * The session that issued the query.
     */
    private final Session session;

    /**
     * The select properties.
     */
    private final QName[] properties;

    /**
     * The namespace resolver of the session.
     */
    private final NamespaceResolver resolver;

    /**
     * Creates a new <code>WorkspaceTraversalResult</code>.
     *
     * @param session    the session that issued the query.
     * @param properties the select properties.
     * @param resolver   the namespace resolver of the session.
     */
    public WorkspaceTraversalResult(Session session,
                             QName[] properties,
                             NamespaceResolver resolver) {
        this.session = session;
        this.properties = properties;
        this.resolver = resolver;
    }

    /**
     * @inheritDoc
     */
    public String[] getColumnNames() throws RepositoryException {
        try {
            String[] propNames = new String[properties.length];
            for (int i = 0; i < properties.length; i++) {
                propNames[i] = properties[i].toJCRName(resolver);
            }
            return propNames;
        } catch (NoPrefixDeclaredException npde) {
            String msg = "encountered invalid property name";
            throw new RepositoryException(msg, npde);

        }
    }

    /**
     * @inheritDoc
     */
    public RowIterator getRows() throws RepositoryException {
        return new RowIteratorImpl(getNodeIterator(), properties, resolver);
    }

    /**
     * @inheritDoc
     */
    public NodeIterator getNodes() throws RepositoryException {
        return getNodeIterator();
    }

    /**
     * Returns a {@link ScoreNodeIterator} that traverses the workspace in
     * document order.
     *
     * @return iterator that returns nodes in document order.
     * @throws RepositoryException if an error occurs while creating the
     *                             iterator.
     */
    private ScoreNodeIterator getNodeIterator() throws RepositoryException {
        return new TraversingNodeIterator(session.getRootNode(), Offset.ZERO);
    }

    /**
     * Implements a node iterator that traverses the workspace in document
     * order.
     */
    private class TraversingNodeIterator implements ScoreNodeIterator, Offset {

        /**
         * The current <code>Node</code>, which acts as the starting point for
         * the traversal.
         */
        private final Node currentNode;

        /**
         * The chain of iterators which includes the iterators of the children
         * of the current node.
         */
        private IteratorChain selfAndChildren;

        /**
         * Offset of this iterator to calculate the value in
         * {@link #getPosition()}.
         */
        private final Offset offset;

        /**
         * Current (local) position in this iterator.
         */
        private long position;

        /**
         * Creates a <code>TraversingNodeIterator</code>.
         * @param start the node from where to start the traversal.
         * @param offset the offset to use to calculate the position.
         */
        TraversingNodeIterator(Node start, Offset offset) {
            currentNode = start;
            this.offset = offset;
        }

        /**
         * Returns always 1.
         * @return always 1.
         */
        public float getScore() {
            return 1.0f;
        }

        /**
         * @inheritDoc
         */
        public NodeImpl nextNodeImpl() {
            init();
            NodeImpl n = (NodeImpl) selfAndChildren.next();
            position++;
            return n;
        }

        /**
         * @inheritDoc
         */
        public Node nextNode() {
            return nextNodeImpl();
        }

        /**
         * @inheritDoc
         */
        public void skip(long skipNum) {
            while (skipNum > 0) {
                if (hasNext()) {
                    next();
                    skipNum--;
                } else {
                    throw new NoSuchElementException();
                }
            }
        }

        /**
         * Returns always -1 (unknown).
         * @return always -1.
         */
        public long getSize() {
            return -1;
        }

        /**
         * @inheritDoc
         */
        public long getPosition() {
            return offset.getValue() + position;
        }

        /**
         * @exception UnsupportedOperationException always.
         */
        public void remove() {
            throw new UnsupportedOperationException("remove");
        }

        /**
         * @inheritDoc
         */
        public boolean hasNext() {
            init();
            return selfAndChildren.hasNext();
        }

        /**
         * @inheritDoc
         */
        public Object next() {
            return nextNode();
        }

        /**
         * Returns the offset value for this iterator. The offset is the current
         * position plus 1.
         * @return  the offset value for this iterator.
         */
        public long getValue() {
            return getPosition() + 1;
        }

        /**
         * Initializes the iterator chain once.
         */
        private void init() {
            if (selfAndChildren == null) {
                Iterator current = Arrays.asList(new Node[]{currentNode}).iterator();
                List allIterators = new ArrayList();
                allIterators.add(current);
                Offset offset = new Offset() {
                    public long getValue() {
                        return TraversingNodeIterator.this.offset.getValue() + 1;
                    }
                };

                // create new TraversingNodeIterator for each child
                try {
                    NodeIterator children = currentNode.getNodes();
                    while (children.hasNext()) {
                        offset = new TraversingNodeIterator(children.nextNode(), offset);
                        allIterators.add(offset);
                    }
                } catch (RepositoryException e) {
                    // currentNode is probably stale
                }
                selfAndChildren = new IteratorChain(allIterators);
            }
        }
    }

    /**
     * Helper class that holds an offset value.
     */
    interface Offset {

        /**
         * Offset that always returns the value 0.
         */
        static final Offset ZERO = new Offset() {
            public long getValue() {
                return 0;
            }
        };

        /**
         * Returns the value of this offset.
         * @return the value of this offset..
         */
        long getValue();
    }
}
